\chapter{\texttt{std::any}}\label{ch17}
一般来说，C++是一门类型绑定和类型安全的语言。
值对象被声明为确定的类型，这个类型定义了所有可能的操作、也定义了对象的行为。
而且，对象不能改变自身的类型。

\texttt{std::any}是一种在保证类型安全的基础上还能改变自身类型的值类型。
也就是说，它可以持有任意类型的值，并且它知道自己当前持有的值是什么类型的。
当声明一个这种类型的对象时不需要指明所有可能的类型。

实现的关键在于\texttt{std::any}对象同时包含了值和值的类型。
因为内含的值可以有任意的大小，所以可能会在堆上分配内存。
然而，实现应该尽量避免为小类型的值例如\texttt{int}在堆上分配内存。

对于一个\texttt{std::any}对象，如果你赋值为一个字符串，它将会分配内存并拷贝字符串，
并存储记录当前的值是一个字符串。之后，可以使用运行时检查来判断当前的值的类型。
为了将当前的值转换为真实的类型，必须要使用\texttt{any\_cast<>}。

和\texttt{std::optional<>、std::variant<>}一样，\texttt{std::any}对象有值语义。
也就是说，拷贝被实现为\emph{深拷贝}，会创建一个在自己的内存中持有当前值的独立对象。
因为可能会使用堆内存，所以拷贝\texttt{std::any}的开销一般都很大。
更推荐以引用传递对象，或者move值。\texttt{std::any}支持部分move语义。


\section{使用\texttt{std::any}}
下面的示例演示了\texttt{std::any}的核心能力：
\begin{lstlisting}
    std::any a;             // a为空
    std::any b = 4.3;       // b有类型为double的值4.3
    a = 42;                 // a有类型为int的值42
    b = std::string{"hi"};  // b有类型为std::string的值"hi"

    if (a.type() == typeid(std::string)) {
        std::string s = std::any_cast<std::string>(a);
        useString(s);
    }
    else if (a.type() == typeid(int)) {
        useInt(std::any_cast<int>(a));
    }
\end{lstlisting}
你可以声明一个空的\texttt{std::any}，也可以用某个类型的值初始化。
如果传递了初始值，\texttt{std::any}内含的值的类型将变为初始值的类型。

通过使用成员函数\texttt{type()}，你可以检查内含值的类型和某一个类型的ID是否相同。
如果对象是空的，类型ID将等于\texttt{typeid(void)}。

为了访问内部的值，你必须使用\texttt{std::any\_cast<>}将它转换为真正的类型：
\begin{lstlisting}
    auto s = std::any_cast<std::string>(a);
\end{lstlisting}
如果转换失败，可能是因为对象为空或者与内部值的类型不匹配，这时会抛出一个
\texttt{std::bad\_any\_cast}异常。因此，在不知道当前类型的情况下，
你最好像下面这样使用：
\begin{lstlisting}
    try {
        auto s = std::any_cast<std::string>(a);
        ...
    }
    catch (std::bad_any_cast& e) {
        std::cerr << "EXCEPTION: " << e.what() << '\n';
    }
\end{lstlisting}
注意\texttt{std::any\_cast<>}会创建一个指定类型的对象。
例如这个例子中，如果你用\texttt{std::string}作为\texttt{std::\\
any\_cast<>}的模板参数，
它将创建一个临时的字符串(prvalue)，然后使用临时字符串初始化新的对象\texttt{s}。
如果不需要初始化其他变量，更推荐转换为引用类型来避免创建临时对象：
\begin{lstlisting}
    std::cout << std::any_cast<const std::string&>(a);
\end{lstlisting}
如果想要修改当前的值，也需要转换为相应的引用类型：
\begin{lstlisting}
    std::any_cast<std::string&>(a) = "world";
\end{lstlisting}
你也可以对一个\texttt{std::any}对象的地址调用\texttt{std::any\_cast}。
这种情况下，如果类型匹配那么转换结果将会是一个相应类型的指针，否则将返回\texttt{nullptr}：
\begin{lstlisting}
    auto p = std::any_cast<std::string>(&a);
    if (p) {
        ...
    }
\end{lstlisting}
或者，可以使用新的\nameref{ch2.1}：
\begin{lstlisting}
    if (auto p = std::any_cast<std::string>(&a); p != nullptr) {
        ...
    }
\end{lstlisting}
或者：
\begin{lstlisting}
    if (auto p = std::any_cast<std::string>(&a)) {
        ...
    }
\end{lstlisting}
为了清空一个\texttt{std::any}对象，你可以调用：
\begin{lstlisting}
    a.reset();   // 清空对象
\end{lstlisting}
或者：
\begin{lstlisting}
    a = std::any{};
\end{lstlisting}
或者：
\begin{lstlisting}
    a = {};
\end{lstlisting}
你也可以直接检查对象是否有值：
\begin{lstlisting}
    if (a.has_value()) {
        ...
    }
\end{lstlisting}
注意存储值时类型会退化（数组转换为指针，顶层引用和\texttt{const}被忽略）。
对于字符串字面量，值类型将是\texttt{const char*}。
为了使用\texttt{std::any\_cast<>}进行转换，你必须显式指明这个类型：
\begin{lstlisting}
    std::any a = "hello";   // type()是const char*
    if (a.type() == typeid(const char*)) {  // true
        ...
    }
    if (a.type() == typeid(std::string)) {  // false
        ...
    }
    std::cout << std::any_cast<const char*>(a) << '\n'; // OK
    std::cout << std::any_cast<std::string>(a) << '\n'; // EXCEPTION
\end{lstlisting}
这基本就是\texttt{std::any}支持的所有操作了。
没有定义比较运算符（这意味着你不能比较或者排序对象）。
没有定义哈希函数，也没有定义\texttt{value()}成员函数。
而且，因为类型只有在运行时才能获取，所以也不能使用泛型lambda来独立于类型处理当前的值。
你只能使用运行时的\texttt{std::any\_cast<>}函数来处理当前的值，
这意味着处理当前值时你需要一些类型特定的代码来重入C++的类型系统。

然而，你可以把\texttt{std::any}对象放置在容器中。例如：
\begin{lstlisting}
    std::vector<std::any> v;

    v.push_back(42);
    std::string s = "hello";
    v.push_back(s);

    for (const auto& a : v) {
        if (auto pa = std::any_cast<const std::string>(&a); pa != nullptr) {
            std::cout << "string: " << *pa << '\n';
        }
        else if (auto pa = std::any_cast<const int>(&a); pa != nullptr) {
            std::cout << "int: " << *pa << '\n';
        }
    }
\end{lstlisting}
注意你应该总是使用这样的if-else链。这里不能使用\texttt{switch}语句。


\section{\texttt{std::any}类型和操作}
这一节详细描述\texttt{std::any}的类型和操作。

\subsection{Any类型}
在头文件<any>中，C++标准库以如下方式定义了类\texttt{std::any}：
\begin{lstlisting}
    namespace std {
        class any;
    }
\end{lstlisting}
也就是说，\texttt{std::any}根本就不是模板类。

另外，还定义了下面的类型和对象：
\begin{itemize}
    \item 异常类\texttt{std::bad\_any\_cast}，当转换失败时会抛出这种异常。
    这个类派生自\texttt{std::bad\_cast}，后者又派生自\texttt{std::exception}。
\end{itemize}
\texttt{std::any}类也使用了定义在头文件\texttt{<utility>}中的
\texttt{std::in\_place\_type}对象（类型是\texttt{std::in\_place\\
\_type\_t}）。

\subsection{Any操作}
表\hyperref[t17.1]{\texttt{std::any}的操作}列出了\texttt{std::any}的所有操作：
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{操作}             & \textbf{效果}                                            \\
        \hline
        \emph{构造函数}             & 创建一个any对象（可能会调用底层类型的构造函数）                              \\
        \texttt{make\_any<>()}  & 创建一个any对象（传递参数来初始化）                                    \\
        \emph{析构函数}             & 销毁any对象                                                \\
        \texttt{=}              & 赋予新值                                                   \\
        \texttt{emplace<T>()}   & 赋予一个类型\texttt{T}的新值                                    \\
        \texttt{reset()}        & 销毁any类型（使对象变为空）                                        \\
        \texttt{has\_value()}   & 返回对象是否持有值                                              \\
        \texttt{type()}         & 以\texttt{std::type\_info}对象返回当前类型                      \\
        \texttt{any\_cast<T>()} & 将当前值转换为类型\texttt{T}的值（如果类型不正确将抛出异常/返回\texttt{nullptr}） \\
        \texttt{swap()}         & 交换两个对象的值                                               \\
        \hline
    \end{tabular}
    \caption{\texttt{std::any}的操作}
    \label{t17.1}
\end{table}

\subsubsection{构造函数}
默认情况下，\texttt{std::any}被初始化为空。
\begin{lstlisting}
    std::any a1;        // a1是空的
\end{lstlisting}
如果传递值来初始化，内含值的类型将是它\emph{退化}后的类型：
\begin{lstlisting}
    std::any a2 = 42;       // a2包含int类型的值
    std::any a3 = "hello";  // a2包含const char*类型的值
\end{lstlisting}
为了使内部值的类型和初始值的类型不同，你需要使用\texttt{in\_place\_type}标记：
\begin{lstlisting}
    std::any a4{std::in_place_type<long>, 42};
    std::any a5{std::in_place_type<std::string>, "hello"};
\end{lstlisting}
传给\texttt{in\_place\_type}的类型也可能退化。
下面的代码声明了一个持有\texttt{const char*}的对象：
\begin{lstlisting}
    std::any a5b{std::in_place_type<const char[6]>, "hello"};
\end{lstlisting}
为了用多个参数初始化\texttt{std::any}对象，你必须手动创建对象或者你可以添加
\texttt{std::in\_place\_type}作为第一个参数（因为内含类型不能直接从多个初始值推导出来）：
\begin{lstlisting}
    std::any a6{std::complex{3.0, 4.0}};
    std::any a7{std::in_place_type<std::complex<double>>, 3.0, 4.0};
\end{lstlisting}
你甚至可以传递初值列和其他参数：
\begin{lstlisting}
    // 用一个以lambda为排序准则的set初始化std::any对象
    auto sc = [] (int x, int y) {
                  return std::abs(x) < std::abs(y);
              };
    std::any a8{std::in_place_type<std::set<int, decltype(sc)>>, {4, 8, -7, -2, 0, 5}, sc};
\end{lstlisting}
注意还有一个快捷函数\texttt{make\_any<>()}，它可以接受一个或多个参数
（不需要使用\texttt{in\_place\_type}参数）。
你必须显式指明初始化的类型（即使只有一个参数它也不会自动推导类型）：
\begin{lstlisting}
    auto a10 = std::make_any<float>(3.0);
    auto a11 = std::make_any<std::string>("hello");
    auto a13 = std::make_any<std::complex<double>>(3.0, 4.0);
    auto a14 = std::make_any<std::set<int, decltype(sc)>> ({4, 8, -7, -2, 0, 5}, sc);
\end{lstlisting}

\subsubsection{修改值}
可以使用赋值操作和\texttt{emplace()}来修改值。例如：
\begin{lstlisting}
    std::any a;

    a = 42;         // a含有int类型的值
    a = "hello";    // a含有const char*类型的值
    a.emplace<std::string>("hello");            // a含有std::string类型的值
    a.emplace<std::complex<double>>(4.4, 5.5);  // a含有std::complex<double>类型的值
\end{lstlisting}

\subsubsection{访问值}
为了访问内含的值，你必须使用\texttt{std::any\_cast<>}将它转换为真实的类型。
例如为了把值转换成string，你有以下几种方法：
\begin{lstlisting}
    std::any_cast<std::string>(a);        // 返回值的拷贝

    std::any_cast<std::string&>(a);       // 通过引用获取写权限

    std::any_cast<const std::string&>(a); // 通过引用获取读权限
\end{lstlisting}
如果移除掉顶层引用和\texttt{const}之后类型ID相同则说明类型匹配。
如果转换失败，将会抛出一个\texttt{std::bad\_\\
any\_cast}异常。

为了避免异常处理，你可以传递\texttt{any}对象的地址。
当因为类型不匹配导致转换失败时它会返回\texttt{nullptr}：
\begin{lstlisting}
    if (auto sp{std::any_cast<std::string>(&a)}; sp != nullptr) {
        ... // 使用*sp获取a的值的写权限
    }

    if (auto sp{std::any_cast<const std::string>(&a)}; sp != nullptr) {
        ... // 使用*sp获取a的值的读权限
    }
\end{lstlisting}
注意，这里如果转换为引用将导致运行时错误：
\begin{lstlisting}
    std::any_cast<std::string&>(&a);    // 运行时错误
\end{lstlisting}

\subsubsection{move语义}
\texttt{std::any}也支持move语义。然而，注意前提是底层类型要支持拷贝语义。
也就是说\textbf{move-only类型}不支持作为内含的值类型。

处理move语义的最佳方式可能不是很明显，你可以这样做：
\begin{lstlisting}
    std::string s("hello, world!");

    std::any a;
    a = std::move(s);   // 把s移进a

    s = std::move(std::any_cast<std::string&>(a));  // 把a中的string移动到s
\end{lstlisting}
注意像通常一样，值被移走的对象处于仍然有效但是值未定义的状态。
因此，你可以继续将\texttt{a}用作字符串，只要不对它的值做任何假设。
下面的语句将\emph{不会}输出\texttt{"NIL"}，值被移走的字符串一般是空字符串
（但也可能有其他的值）：
\begin{lstlisting}
    std::cout << (a.has_value() ? std::any_cast<std::string>(a) : std::string("NIL"));
\end{lstlisting}
注意：
\begin{lstlisting}
    s = std::any_cast<std::string>(std::move(a));
\end{lstlisting}
也能生效，但需要一次额外的move。

直接转换成右值引用将不能通过编译：
\begin{lstlisting}
    s = std::any_cast<std::string&&>(a);    // 编译期error
\end{lstlisting}
注意和如下调用
\begin{lstlisting}
    a = std::move(s);   // 把s移进a
\end{lstlisting}
相比，下面的代码\emph{有可能不能}工作（即使这是C++标准里的一个例子）：
\begin{lstlisting}
    std::any_cast<string&>(a) = std::move(s); // OOPS：a必须持有string
\end{lstlisting}
只有当\texttt{a}已经包含有一个\texttt{std::string}类型的值时这段代码才能工作。
否则，在我们赋予新值之前，这个转换就会抛出\texttt{std::bad\_any\_cast}异常。


\section{后记}
Any对象由Kevlin Henney和Beman Dawes于2006年在\url{https://wg21.link/n1939}
中首次提出，并指定Boost.Any作为参考实现。这个类因Beman Dawes、Kevlin Henney、
Daniel Krügler在\url{https://wg21.link/n3804}中的提案被Library Fundamentals TS采纳。

后来这个类因Beman Dawes和Alisdair Meredith的\url{https://wg21.link/p0220r1}提案
而和其他组件一起被C++17标准采纳。

Vicente J.Botet Escriba在\url{https://wg21.link/p0032r3}中统一
了\texttt{std::any}和\texttt{std::variant<>}以及
\texttt{std::optional<>}的API。Jonathan Wakely在
\url{https://wg21.link/p0504r0}中修正了\texttt{in\_place}标记的行为。